# Grid trading strategies
# 
# Author: Mario
# 
# Version: v.20200424
#
import math
import logging
import datetime
import numpy as np
from hashlib import md5
from functools import reduce
from collections import defaultdict

logger = logging.getLogger("utility")


def hashed(security, timestamp, action, **kwargs):
    """Hash transaction for uniqueness check"""
    hash_string = "{}&{}&{}".format(security, timestamp, action)
    return md5(hash_string.encode()).hexdigest()


def unify_digits(digit: float, unit: int):
    return round(digit, int(math.log10(unit)))


def make_transaction(security, price, unit, quantity, timestamp, action, **kwargs):
    transaction = dict(security=security,
                       price=unify_digits(price, unit),
                       quantity=int(quantity),
                       timestamp=timestamp,
                       action=action)
    transaction.update(dict(volume=unify_digits(transaction["price"] * transaction["quantity"], unit)))
    return transaction


def get_timestamp(level="d"):
    if level in {"d", "day"}:
        return datetime.datetime.now().strftime("%Y%m%d")

    elif level in {"h", "hour"}:
        return datetime.datetime.now().strftime("%Y%m%d%H")

    elif level in {"m", "minute"}:
        return datetime.datetime.now().strftime("%Y%m%d%H%M")

    elif level in {"s", "second"}:
        return datetime.datetime.now().strftime("%Y%m%d%H%M%S")

    else:
        assert False, "Invalid timestamp fetching"


class BaseSignal:
    # 日间交易信号是用于计算交易日`前`仓位控制策略或买入卖出策略的对象:
    # 决策变量:
    # 1) grid_size: 网格大小
    var_grid_size = int(0)
    # 2) grid_premium: 网格价差
    var_grid_premium = float(0)
    # 3) min_price: 最低价
    var_min_price = float(0)
    # 4) max_price: 最高价
    var_max_price = float(0)
    # 保留基准参数，在没有策略设置的情况下默认运行基准参数:
    base_grid_size = int(0)
    base_grid_premium = float(0)
    base_min_price = float(0)
    base_max_price = float(0)
    # 交易单位的幂次
    power = int(0)
    # 默认的参数
    default_params = dict(base_grid_size=1000,
                          base_grid_premium=0.010,
                          base_min_price=0.001,
                          base_max_price=1.000,
                          power=3)

    def __init__(self):
        pass

    def _set_parameters(self, **params):
        for k, v in params.items():
            if hasattr(self, k):
                setattr(self, k, v)
        return self

    def rebase_parameters(self):
        # 将var_*的参数校准回base_*的状态
        for attr in dir(self):
            if attr.startswith("var"):
                self._set_parameters(**{attr: getattr(self, attr.replace("var", "base"))})

    def _ok(self):
        status = []
        for attr in dir(self):
            try:
                if any([attr.startswith("var"), attr.startswith("base")]):
                    assert getattr(self, attr), "当信号参数中存在零值时，无法判断信号"
            except AssertionError:
                status += ["配置错误: %s 为零" % attr]
        logger.error("\n".join(status))
        return False if status else True

    @staticmethod
    def _get_share(command: dict):
        key = reduce(lambda x: x, command)
        return abs(int(command[key] / key))

    @staticmethod
    def make_order(orders, price, grid_size, premium):
        # volume: 负值为卖，正值为买
        # 注意: 输出的交易指令只能是包含当前现价(price)的集合而非订单簿(orders)里的价格
        commands = {}
        sell_threshold = price - premium
        for order_price, order_quantity in orders.items():
            if (price >= sell_threshold and price > order_price) and order_quantity:
                # 存在仓位且符合卖出阈值:
                commands[order_price] = {price: -(order_quantity * price)}
            elif price <= order_price and not order_quantity:
                # 高于当前价格的仓位在订单簿里有空缺时可以买入补足:
                commands[order_price] = {price: (grid_size * price)}

        return commands

    def fetch_orders(self, account, price, typ):
        """基础信号器
        :param account: 账户对象(Account)
        :param price: 当前价格(price)
        :param typ: 历史账户(hist) / 每日账户(daily)

        :return: 返回基础交易指令
        """
        assert typ in ["hist", "daily"], "无效的交易类型"
        # 得到对应交易类型的查询函数
        query = getattr(account, typ + "_query")
        orders = query(price)
        return self.make_order(orders, self.var_grid_size, self.var_grid_premium)


class InterSignal(BaseSignal):
    """日间交易信号器
    主要对盘前的行情和账户进行分析，给出下一个交易日的策略参数
    """

    def __init__(self, power, **params):
        self._set_parameters(**params)
        # `power` 幂次
        self.power = power

    def _bull_market(self):
        pass

    def _bear_market(self):
        pass

    def _update(self):
        pass

    def fetch(self, price):
        """给出优化后的交易策略参数
        :return: 返回 dict(var_* = *) 包含交易参数的字典
        """
        pass


class IntraSignal:
    """日内交易信号"""

    def __init__(self):
        pass


class Account:
    """账户(Account)是用于更新仓位和查询信号的对象
    1) 分别在盘前和盘中记录仓位情况, 同时进行仓位的管理
    2) hist_account 历史账户主要记录日前所有仓位的信息，以便在盘前函数中做规划
    3) daily_account 是日内账户，主要记录日内所有仓位的信息，每日刷新
    账户结构:
    1) 历史账户: dict(int)
    2) 日内账户: dict(int)
    """

    def __init__(self, capital, logger, min_, max_, power):
        # 账户本金:
        self.capital = capital
        # 获得历史价格区间:
        self.min_price = min_
        self.max_price = max_
        # 格子的幂次单位:
        self.power = power
        self.unit = 10 ** power
        # 日志记录:
        self.logger = logger
        # 生成历史账户的价格序列:
        assert self.min_price != self.max_price, \
            "invalid price range: [%.3f, %.3f]" % (self.min_price, self.max_price)
        # 生成历史账户:
        self.hist_account = {price: 0 for price in self._sequence_prices(min_, max_)}
        self.daily_account = defaultdict(int)

    def _round(self, price):
        return round(price, self.power)

    def _sequence_prices(self, low, high):
        # :return: 返回一个价格序列(float)
        return np.arange(int(low * 10 ** self.power),
                         int(high * 10 ** self.power),
                         1) / 10 ** self.power

    def _get_all_prices(self):
        # :return: 返回历史账户中的价格数组
        return np.array(list(self.hist_account.keys()))

    def _get_all_quantities(self):
        # :return: 返回历史账户中的持股数组
        return np.array(list(self.hist_account.values()))

    def update_capital(self, capital_flow):
        self.capital = capital_flow if capital_flow > 0 else 0

    def can_trade(self, volume):
        # 查询本金是否满足交易条件:
        return True if self.capital >= volume else False

    def update_price_range(self, min_, max_):
        self.min_price = min_
        self.max_price = max_
        # 修改账户序列, 当且仅当价格序列不发生冲突时, 冲突存在于
        for price in self._sequence_prices(min_, max_):
            self.hist_account[price] = 0 if price not in self.hist_account else self.hist_account[price]
        # 当修改的价格小于原价格序列的最高值且仍有持仓时:
        self.logger.info("[alter price] min: %.3f, max: %.3f" % (min_, max_))

    def _record_check(self, record):
        assert record.get("quantity", 0), "invalid quantity: %s" % str(record)
        assert record.get("price", 0), "invalid price: %s" % str(record)
        # 统一价格和股数的小数位:
        record["price"] = round(record["price"], self.power)
        record["quantity"] = int(record["quantity"])
        return record

    def hist_update(self, record):
        # 对于历史账户的修改只能通过update函数进行
        # 传入的record结构: dict(price=float, quantity=int)
        # 每次更新前，先对可能存在的重复价格进行合并:
        price = round(record["price"], self.power)
        quantity = int(record["quantity"])
        if quantity == 0:
            self.hist_account[price] = 0
        elif quantity > 0:
            if price not in self.hist_account:
                self.hist_account[price] = 0
            self.hist_account[price] += quantity

    def daily_update(self, record):
        self._record_check(record)
        self.daily_account[record["price"]] += record["quantity"]

    def daily_cleanup(self):
        # 归零当日账户并添加到历史账户中:
        # :param today: 格式为 "20001010"
        for price, quantity in self.daily_account.items():
            self.hist_update(dict(price=price, quantity=quantity))
        # 日内账户归零:
        self.daily_account = defaultdict(int)

    def _query(self, price):
        # 历史账户的查询操作:
        # 1) 价格落在区间中时，向下求和;
        # 2) 价格超出区间时:
        #   a) 低于区间给出信号"补仓-merge", 满足条件时购入低价格子;
        #   b) 高于区间时给出价差信号"价差-gap", 如果满足出清条件则清仓.
        # 3) 明确返回的格式: (可以根据策略对购入量实行梯度分布)
        """ 当存在价差时的账户查询返回一个标准的JSON对象
        {0.500: 100,
         ... ...
         0.510: 100}
        """
        prices = self._get_all_prices()
        if price > prices.max():
            return {price: int(self._get_all_quantities().sum())}

        elif price < prices.min():
            # 默认为以股为单位(1/100为1手), 并且每次扣100股作为交易费防止本金不足
            bulk = int((self.capital // (price * 100)) - 1)
            if bulk > 1:
                # 交易一半的仓位
                return {price: bulk * (100 / 2)}
            else:
                # 正常买入格子数量
                return {price: 0}

        else:
            return {price: self.hist_account.get(price, 0)}

    def daily_query(self, price):
        if price > self._get_all_prices().max():
            return {price: -1}
        price = round(price, self.power)
        return {price: self.daily_account.get(price, 0)}

    def hist_query(self, price):
        return self._query(price)
